package com.lipaluma.field.mapper.single.impl;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.lang.StringUtils;

import com.lipaluma.annotations.config.ValidationStrategy;
import com.lipaluma.annotations.validations.ValidateCollection;
import com.lipaluma.context.Context;
import com.lipaluma.exceptions.CsvParserException;
import com.lipaluma.exceptions.CsvParserValidationException;
import com.lipaluma.field.adaptor.Adaptor;
import com.lipaluma.field.adaptor.AdaptorFactory;
import com.lipaluma.field.adaptor.ChainAdaptor;
import com.lipaluma.field.mapper.FieldAdaptedOnEachValue;
import com.lipaluma.field.mapper.FieldValidated;
import com.lipaluma.field.mapper.FieldValidatedOnEachValue;
import com.lipaluma.field.mapper.mapping.MappingInfo;
import com.lipaluma.field.mapper.mapping.MappingOptions;
import com.lipaluma.field.mapper.single.AbstractSingleFieldMapper;
import com.lipaluma.field.validator.ChainValidator;
import com.lipaluma.field.validator.ValidationBasicOptions;
import com.lipaluma.field.validator.ValidationError;
import com.lipaluma.field.validator.Validator;
import com.lipaluma.field.validator.ValidatorFactory;
import com.lipaluma.field.validator.strategies.ValidationStrategyEnum;
import com.lipaluma.io.Row;
import com.lipaluma.util.ParserUtils;
import com.lipaluma.util.ReflectionUtils;
import com.lipaluma.xml.mapping.XmlFieldMapping;

/**
 * Create a Collection column where each value is separated by a separator
 * @author Mario
 */
public class CollectionFieldMapper extends AbstractSingleFieldMapper implements FieldValidated, FieldValidatedOnEachValue, FieldAdaptedOnEachValue {

	private ChainAdaptor chainAdaptorOnEachValue;
	private ChainValidator chainValidatorOnEachValue;
	private ChainValidator chainValidator;
	private String separator;
	private String pattern;
	private boolean keepAll;
	private ValidationStrategyEnum validationStrategy = ValidationStrategyEnum.LET_FINISH_AND_SHOW_ERRORS;
	
	public CollectionFieldMapper() {}
	
	/**
	 * Field Mapper used to map a collection where each value is separated by a String 'separator'
	 * @param mapping information of mapping
	 * @param separator separator of each value
	 */
	public CollectionFieldMapper(MappingInfo mapping, String separator) {
		super(mapping);
		if(StringUtils.isEmpty(separator))
			throw new CsvParserException("Error for initialisation of the field mapper "+this.getClass().getSimpleName()+" with mapping "+mapping+" : The value of separator cannot be empty");
		this.separator = separator;
	}
	
	/**
	 * Field Mapper used to map a collection where each value is separated by a String 'separator' and parsing each values with the pattern
	 * @param columnIndex index of column
	 * @param separator separator of each value
	 * @param pattern pattern format of each value
	 */
	public CollectionFieldMapper(MappingInfo mapping, String separator, String pattern) {
		this(mapping, separator);
		this.pattern = pattern;
	}
	
	/**
	 * Field Mapper used to map a collection where each value is separated by a String 'separator' and parsing each values with the pattern.<br/>
	 * We can specify here the option to keep all empty values between separators. By default, these values are not kept.
	 * @param columnIndex index of column
	 * @param separator separator of each value
	 * @param pattern pattern format of each value
	 * @param keepAll keep All values, even null or empty
	 */
	public CollectionFieldMapper(MappingInfo mapping, String separator, String pattern, boolean keepAll) {
		this(mapping, separator, pattern);
		this.keepAll = keepAll;
	}
	
	/**
	 * Field Mapper used to map a collection where each value is separated by a String 'separator'
	 * @param mapping information of mapping
	 * @param separator separator of each value
	 */
	public CollectionFieldMapper(MappingInfo mapping, String separator, ValidationStrategyEnum strategy) {
		super(mapping);
		if(StringUtils.isEmpty(separator))
			throw new CsvParserException("Error for initialisation of the field mapper "+this.getClass().getSimpleName()+" with mapping "+mapping+" : The value of separator cannot be empty");
		this.separator = separator;
		this.validationStrategy  = strategy;
	}
	
	/**
	 * Field Mapper used to map a collection where each value is separated by a String 'separator' and parsing each values with the pattern
	 * @param columnIndex index of column
	 * @param separator separator of each value
	 * @param pattern pattern format of each value
	 */
	public CollectionFieldMapper(MappingInfo mapping, String separator, String pattern, ValidationStrategyEnum strategy) {
		this(mapping, separator, strategy);
		this.pattern = pattern;
	}
	
	/**
	 * Field Mapper used to map a collection where each value is separated by a String 'separator' and parsing each values with the pattern.<br/>
	 * We can specify here the option to keep all empty values between separators. By default, these values are not kept.
	 * @param columnIndex index of column
	 * @param separator separator of each value
	 * @param pattern pattern format of each value
	 * @param keepAll keep All values, even null or empty
	 */
	public CollectionFieldMapper(MappingInfo mapping, String separator, String pattern, boolean keepAll, ValidationStrategyEnum strategy) {
		this(mapping, separator, pattern, strategy);
		this.keepAll = keepAll;
	}

	@Override
	public void init(Context context, Field field) {
		super.init(context, field);
		if(field.isAnnotationPresent(ValidationStrategy.class))
			this.validationStrategy = field.getAnnotation(ValidationStrategy.class).value();
		initAdaptorOnEachValue(context, field);
		initValidatorOnEachValue(context, field);
		initValidator(context, field);
	}
	
	@Override
	public void init(Context context, Field field, XmlFieldMapping mapping) {
		super.init(context, field, mapping);
		initAdaptorOnEachValue(context, field, mapping);
		initValidatorOnEachValue(context, field, mapping);
		initValidator(context, field, mapping);
	}

	@Override
	protected void init(MappingOptions mappingOptions) {
		this.separator = mappingOptions.getSeparator();
		if(this.separator == null || this.separator.equals(""))
			throw new CsvParserException("Error on field "+field.getName()+" : The separator must be initialized for array or collection field");
		this.keepAll = mappingOptions.isKeepAll();
		this.pattern = mappingOptions.getPattern();
	}

	@Override
	public void addNewAdaptorOnEachValue(Adaptor adaptor) {
		if(chainAdaptorOnEachValue == null)
			chainAdaptorOnEachValue = new ChainAdaptor();
		chainAdaptorOnEachValue.push(adaptor);
	}

	@Override
	public void addNewValidator(Validator validator) {
		if(chainValidator == null)
			chainValidator = new ChainValidator(false);
		chainValidator.push(validator);
	}

	@Override
	public void addNewValidatorOnEachValue(Validator validator) {
		if(chainValidatorOnEachValue == null)
			chainValidatorOnEachValue = new ChainValidator(false);
		chainValidatorOnEachValue.push(validator);
	}

	@Override
	public void initValidator(Context context, Field field) {
		ValidationBasicOptions validationOptions = ValidationBasicOptions.builder().byAnnotation(field.getAnnotation(ValidateCollection.class)).build();
		this.chainValidator = ValidatorFactory.createNewChainValidator(context, field, validationOptions);
	}

	@Override
	public void initValidator(Context context, Field field, XmlFieldMapping mapping) {
		this.chainValidator = ValidatorFactory.createNewChainValidator(context, field, mapping);
	}
	
	@Override
	public void required(boolean required) {
		chainValidator.setRequired(required);
	}

	@Override
	public void initValidatorOnEachValue(Context context, Field field) {
		Class<? extends Annotation> clazz = ValidatorFactory.getValidationAnnotationByType(ReflectionUtils.getParameterizedClass(field));
		ValidationBasicOptions validationOptions = ValidationBasicOptions.builder().byAnnotation(field.getAnnotation(clazz)).build();
		this.chainValidatorOnEachValue = ValidatorFactory.createNewChainValidatorOnEachValue(context, field, validationOptions);
	}
	
	@Override
	public void initValidatorOnEachValue(Context context, Field field, XmlFieldMapping mapping) {
		this.chainValidatorOnEachValue = ValidatorFactory.createNewChainValidatorOnEachValue(context, field, mapping);
	}
	@Override
	public void eachValueIsRequired(boolean required) {
		chainValidatorOnEachValue.setRequired(required);
	}
	
	@Override
	public void initAdaptorOnEachValue(Context context, Field field) {
		this.chainAdaptorOnEachValue = AdaptorFactory.createNewChainAdaptor(context, field);
	}

	@Override
	public void initAdaptorOnEachValue(Context context, Field field, XmlFieldMapping mapping) {
		this.chainAdaptorOnEachValue = AdaptorFactory.createNewChainAdaptor(context, field, mapping);
	}
	
	@Override
	public Object unmarshal(Row row) {
		String extractedValue = row.extractValue(mapping);
		List<String> values = splitValues(extractedValue);
		List<String> adaptedValues = adaptEachValues(values);
		List<Object> valuesParsed = new ArrayList<Object>();
		for (String adaptedValue : adaptedValues) {
			Object valueParsed = parseExtractedValue(adaptedValue);
			valuesParsed.add(valueParsed);
		}
		
		validateEachValues(valuesParsed);
		validateValue(valuesParsed);
//		Object finalValue = injectValues(valuesParsed);

		if(field.getType().isArray())
			return castAsArray(valuesParsed);
		else
			return valuesParsed;
	}
	
	public Object castAsArray(List<Object> values) {
		Object result = Array.newInstance(field.getType().getComponentType(), values.size());
		for (int i=0; i<values.size(); i++) {
			Array.set(result, i, values.get(i));
		}
		return result;
	}
		
	@Override
	protected Object parseExtractedValue(String value) {
		if(StringUtils.isEmpty(value))
			return null;
		return ParserUtils.parseToTypeOrEnum(ReflectionUtils.getParameterizedClass(field), value, pattern);
	}
	
	@Override
	public void validateEachValues(List<Object> values) throws CsvParserValidationException {
		if(chainValidatorOnEachValue != null) {
			chainValidatorOnEachValue.reset();
			List<Object> valuesValidated = new ArrayList<Object>();
			List<ValidationError> errors = new ArrayList<ValidationError>();
			for (Object object : values) {
				if(!chainValidatorOnEachValue.validate(object, errors)) {
					if(ValidationStrategyEnum.STOP_ON_FIRST_VALIDATION_ERROR.equals(validationStrategy)) {
						StringBuilder message = new StringBuilder(getDescription());
						message.append("An error occured on validation of the value ").append(values).append("\n");
						for (ValidationError error : errors) {
							message.append(error.getMessage()).append("\n");
						}
						throw new CsvParserValidationException(message.toString());
					}
				} else {
					valuesValidated.add(object);
				}
			}
			if(ValidationStrategyEnum.LET_FINISH_AND_SHOW_ERRORS.equals(validationStrategy) && !errors.isEmpty()) {
				StringBuilder message = new StringBuilder(getDescription());
				message.append("An error occured on validation of the value ").append(values).append("\n");
				for (ValidationError error : errors) {
					message.append(error.getMessage()).append("\n");
				}
				throw new CsvParserValidationException(message.toString());
			} else if(ValidationStrategyEnum.FILTER_VALIDATION_ERRORS.equals(validationStrategy)) {
				values = valuesValidated;
			}
		}
	}
	
	@Override
	public void validateValue(Object value) throws CsvParserValidationException {
		if(this.chainValidator != null) {
			List<ValidationError> errors = new ArrayList<ValidationError>();
			if(!chainValidator.validate(value, errors)) {
				StringBuilder message = new StringBuilder(getDescription());
				message.append("An error occured on validation of the value ").append(value).append("\n");
				for (ValidationError error : errors) {
					message.append(error.getMessage()).append("\n");
				}
				throw new CsvParserValidationException(message.toString());
			}
		}
	}
	
	@Override
	@SuppressWarnings("unchecked")
	protected Object formatValueToWriteOnRow(Object value) {
		Collection<Object> values = extractAllValues(value);
		Iterator<Object> iterator = values.iterator();
		if(iterator.hasNext()) {
			StringBuilder valueFormatted = new StringBuilder(String.valueOf(iterator.next()));
			while(iterator.hasNext())
				valueFormatted.append(separator).append(String.valueOf(iterator.next()));
			return valueFormatted;
		} else {
			return "";
		}
	}
	
	private Collection<Object> extractAllValues(Object value) {
		List<Object> result = new ArrayList<Object>();
		if(value.getClass().isArray()) {
			for(int i=0; i<Array.getLength(value); i++) {
				result.add(Array.get(value, i));
			}
		} else if (Collection.class.isAssignableFrom(value.getClass())) {
			Iterator<Object> it = ((Collection<Object>) value).iterator();
			if(it.hasNext()) {
				result.add(it.next());
			}
		}
		return result;
	}
	
//	@SuppressWarnings("unchecked")
//	public Validator initValidator(Field field) {
//		ValidatorOnEachValue validator = initValidatorOnEachValue(field);
//		if(validator != null)
//			validatorOnEachvalue.push(validator);		
//		isThereIncompatiblesValidatorAnnotations(LMin.class, LMax.class, LMinMax.class, DMin.class, DMax.class, DMinMax.class);
//		isThereIncompatiblesValidatorAnnotations(Before.class, After.class, StringLength.class, Matches.class, SubStringRequired.class, SubStringForbidden.class);
//		ChainValidatorAnnotationBuilder builder = new ChainValidatorAnnotationBuilder(field);
//		return builder.byValidators(field).notEmpty(field).withCollectionSize(field).build();
//	}
//
//	public Validator initValidator(Element element, Field field) {
//		ValidatorOnEachValue validator = initValidatorOnEachValue(element, field);
//		if(validator != null)
//			validatorOnEachvalue.push(validator);		
//		isThereIncompatiblesValidatorElements(element, "afterDate", "beforeDate", "minMax", "length", "matches", "subStringsRequired", "subStringsForbidden");
//		ChainValidatorXmlBuilder builder = new ChainValidatorXmlBuilder(element, field);
//		return builder.byValidators(element, field).notEmpty(element, field).withCollectionSize(element, field).build();
//	}

	@Override
	public List<String> adaptEachValues(List<String> values) {
		List<String> adaptedValues = new ArrayList<String>();
		for (String value : values) {
			if(StringUtils.isNotEmpty(value)) {
				String adaptValue = this.chainAdaptorOnEachValue.adapt(value);
				if(StringUtils.isNotEmpty(adaptValue) || keepAll)
					adaptedValues.add(adaptValue);
			} else if (keepAll)
				adaptedValues.add(value);
		}
		return adaptedValues;
	}

	private List<String> splitValues(String value) {
		String[] values;
		if(keepAll)
			values = StringUtils.splitPreserveAllTokens(value, separator);
		else 
			values = StringUtils.split(value, separator);
		return Arrays.asList(values);
	}

}
